Charlie Calvert's C++ Builder Unleashed
- 7 -
Graphics
This chapter covers the basics of graphics programming in the VCL. The VCL encapsulates
the Windows Graphics Device Interface, or GDI. GDI programming can be a subtle and
dangerous process. The VCL tames this technology and makes it extremely easy to use.
In the following pages you will learn about:
- The TCanvas object
- Painting shapes on the screen
- Working with colors
- Working with bitmaps
- Working with metafiles
- Drawing fractals
- Working with fonts
It's important to understand that the basic graphics functionality presented here
is a far cry from the sophisticated tools you find in the DirectX programs seen in
Chapter 28, "Game Programming." This is just the basic functionality needed
to present standard Windows programs. However, the material presented in this chapter
is useful in most standard Windows programs, and it is part of the core knowledge
that all VCL programmers should possess. In particular, it shows how the VCL encapsulates
and simplifies Windows GDI programming.
Graphics.Hpp
The core of the VCL graphics support is found in Graphics.Hpp. The following
objects can be found in this file:
| Object |
Description |
| TCanvas |
This is the basic graphics object used to paint shapes on a form or other surface.
It is the primary wrapper around the GDI. |
| TBrush |
This is the object used to designate the color or pattern that fills in the center
of shapes such as rectangles or ellipses. |
| TPen |
This is the object used for drawing lines, and as the outline for rectangles or ellipses. |
| TPicture |
This is the generalized, high-level VCL wrapper around "pictures" such
as bitmaps, metafiles, or icons. |
| TMetaFileCanvas |
This is the drawing surface for a metafile. |
| TMetaFile |
This is the VCL wrapper around a windows metafile. |
| TBitmap |
This is the VCL wrapper around bitmaps. |
| TIcon |
This is the VCL wrapper around icons. |
| TGraphicsObject |
This is the base class for TBrush, TFont, and TPen. |
| TGraphic |
This is the base class for TMetaFile, TBitmap, and Ticon. |
Many of these objects are explored in the next few pages. In particular, I demonstrate
how to use bitmaps and pens and show how to draw basic geometric shapes to the screen.
You also see how to work with bitmaps and metafiles.
After the TFont object, the next most important graphics object in the
VCL is TCanvas. This is a wrapper around the Windows GDI, or Graphics Device
Interface. The GDI is the subsystem Windows programmers use to paint pictures and
other graphics objects. Most of the content of Graphics.Hpp is aimed at
finding ways to simplify the GDI so that it is relatively easy to use.
Two other important graphics-based objects not found in Graphics.Hpp
are TImage and TPaintBox. Both of these components are covered
in this chapter.
The TColor Type
Almost all the graphics objects use the simple TColor type. This is one
of the building blocks on which the whole graphics system is laid.
For most practical purposes, a variable of type TColor is synonymous
with the built in Windows type COLORREF. However, the actual declaration
for TColor looks like this:
enum TColor {clMin=-0x7fffffff-1, clMax=0x7fffffff};
If you know the COLORREF type, you can see that it is similar to the
TColor type. In a sense, the TColor type is nothing but a set of
predefined COLORREFs.
The Windows palette system enables you to define three different colors, Red,
Green, and Blue, where each color has 255 different shades. These colors are specified
in the last three bytes of the 4-byte long value used to represent a variable of
type TColor.
Here is what the three primary colors look like:
Canvas->Brush->Color = 0x000000FF; // Red
Canvas->Brush->Color = 0x0000FF00; // Green
Canvas->Brush->Color = 0x00FF0000; // Blue
You worked with these colors, and combinations of these colors, in the RGBShape
program from Chapter 2, "Basic Facts about C++Builder."
Of course, it's not convenient to have to write out these numbers directly in
hex. Instead, you can use the RGB macro:
Canvas->Brush->Color = RGB(255, 0, 0); // Red
Canvas->Brush->Color = RGB(0, 255, 0); // Green
Canvas->Brush->Color = RGB(0, 0, 255); // Blue
You can, of course, combine red, green, and blue to produce various shades:
RGB(255, 0, 255); // Purple
RGB(255, 255, 0); // Yellow
RGB(127, 127, 127); // Gray
The VCL also provides a series of constants you can use to specify common colors.
clBlack, for instance, has the same internal number you would obtain from
the following code:
COLORREF clBlack = RGB(0, 0, 0);
Here are some declarations from GRAPHICS.HPP:
#define clBlack TColor(0)
#define clMaroon TColor(128)
#define clGreen TColor(32768)
#define clOlive TColor(32896)
If you want to experiment with this system, you can create a new project in BCB,
drop a button on the main form and run some changes on a method that looks like this:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Canvas->Brush->Color = 0x0000FFFF;
Canvas->Rectangle(0, 0, 200, 200);
}
This code will draw a bright yellow rectangle in the upper right corner of the
main form. Try changing the values of the Brush color to 0x00FF0000,
to RGB(255, 0, 255), to clBlue, and so on. If you get tired of
drawing rectangles, you can switch to ellipses instead:
Canvas->Ellipse(0, 0, 200, 200);
I talk more about drawing shapes with a canvas later in the chapter.
Most of the time you will pick colors from the Color property editor
provided by most components. For instance, you can double click the Color
property for a TForm object to bring up a TColorDialog. However,
there are times when you need to work with the TColor type directly, which
is why I have explained the topic in some detail.
The Canvas Object
All forms, and many components, have a canvas. You can think of a canvas as being
the surface on which graphics objects can paint. In short, the metaphor used by the
developers here is of a painter's canvas.
This TCanvas object is brought into the core component hierarchy through
aggregation, which means in many cases that you access its features via a field of
an object such as a TForm or TImage object. For instance, you can
write the following:
Form1->Canvas->TextOut(1, 1, "Hello from the canvas");
This statement writes the words "Hello from the canvas" in
the upper-left corner of a form. Notice that you do not have to access or reference
a device context directly in order to use this method.
Of course, most of the time you are accessing Form1 from inside of Form1,
so you can write
Canvas->TextOut(1, 1, "Hello from the canvas");
However, you cannot write
Form1->TextOut(1, 1, "Hello from the canvas");
This is because the Canvas object is aggregated into Form1.
TCanvas is not brought in through multiple inheritance. Furthermore, the
aggregation does not attempt to wrap each of the methods of TCanvas inside
methods of TForm. Aggregation is discussed in more detail in Chapters 19,
"Inheritance," and 20, "Encapsulation."
The Canvas object has several key methods that all VCL programmers should
know:
| Arc |
Draw an arc |
| Chord |
A closed figure showing the intersection of a line and an ellipse |
| CopyRect |
Copy an area of one canvas to another |
| Draw |
Draw a bitmap or other graphic on a canvas |
| Ellipse |
Draw an ellipse |
| FillRect |
Fill a rectangle |
| FloodFill |
Fill an enclosed area |
| FrameRect |
Draw a border around a rectangle |
| LineTo |
Draw a line |
| MoveTo |
Draw a line |
| Pie |
Draw a pie-shaped object |
| Polygon |
Draw a multisided object |
| PolyLine |
Connect a set of points on the canvas |
| Rectangle |
Draw a rectangle |
| RoundRect |
Draw a rectangle with rounded corners |
| StretchDraw |
Same as Draw, but stretches the object to fill an area |
| TextHeight |
The height of a string in the current font |
| TextOut |
Output text |
| TextRect |
Output text in a defined area |
| TextWidth |
The width of a string in the current font |
The following properties of the Canvas object are important:
Font
Brush
Pen
Pixels
The following events can be important to some very technical programmers:
OnChange
OnChanging
If you know the Windows API, many of these methods will be familiar to you. The
big gain from using the Canvas object rather than the raw Windows GDI calls
is that the resources you use will be managed for you automatically. In particular,
you never need to obtain a device context, nor do you have to select an object into
a device context.
In some cases, you will get better performance if you write directly to the Windows
GDI. However, it's a mistake to assume that you will always get better by doing so.
For instance, the VCL graphics subsystem will cache and share resources in a sophisticated
manner that would be very difficult to duplicate in your own code. My personal opinion
is that you should use the VCL graphics routines whenever possible, and only turn
to the raw Windows API when you run up against an area of graphics not covered by
the VCL.
The DrawShapes program demonstrates how easy it is to use the TCanvas
object in a program. This application delineates the outlines of a simple paint program
that has the capability to draw lines, rectangles, and ellipses. A screen shot of
the program is shown in Figure 7.1, and the source for the program appears in Listings
7.1 and 7.2.
FIGURE
7.1. The DrawShapes program shows how
to create the lineaments of a simple paint program.
Listing 7.1. The header file for
the DrawShapes program declares an enumerated type and several simple fields.
///////////////////////////////////////
// Main.cpp
// DrawShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include <vcl\Dialogs.hpp>
enum TCurrentShape {csLine, csRectangle, csEllipse};
class TForm1 : public TForm
{
__published:
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *Open1;
TMenuItem *Save1;
TMenuItem *N1;
TMenuItem *Rectangle1;
TMenuItem *Shapes1;
TMenuItem *Rectangle2;
TMenuItem *Ellipse1;
TMenuItem *Colors1;
TMenuItem *Brush1;
TMenuItem *Pen1;
TPanel *Panel1;
TSpeedButton *SpeedButton1;
TSpeedButton *SpeedButton2;
TSpeedButton *SpeedButton3;
TSpeedButton *SpeedButton4;
TMenuItem *Lines1;
TColorDialog *ColorDialog1;
TSpeedButton *SpeedButton5;
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y);
void __fastcall SpeedButton1Click(TObject *Sender);
void __fastcall Brush1Click(TObject *Sender);
void __fastcall Pen1Click(TObject *Sender);
void __fastcall SpeedButton5Click(TObject *Sender);
private:
TRect FShapeRect;
TColor FPenColor;
TColor FBrushColor;
bool FDrawing;
TCurrentShape FCurrentShape;
int FPenThickness;
void __fastcall DrawShape();
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 7.2. The main form for
the DrawShapes program shows how to draw shapes on a canvas.
///////////////////////////////////////
// Main.cpp
// DrawShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FDrawing= False;
FCurrentShape = csRectangle;
FBrushColor = clBlue;
FPenColor = clYellow;
FPenThickness = 1;
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Canvas->Pen->Mode = pmCopy;
DrawShape();
}
void __fastcall TForm1::DrawShape()
{
Canvas->Brush->Color = FBrushColor;
Canvas->Pen->Color = FPenColor;
Canvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
FCurrentShape = TCurrentShape(dynamic_cast<TSpeedButton *>(Sender)->Tag);
}
void __fastcall TForm1::Brush1Click(TObject *Sender)
{
ColorDialog1->Color = FBrushColor;
if (ColorDialog1->Execute())
FBrushColor = ColorDialog1->Color;
}
void __fastcall TForm1::Pen1Click(TObject *Sender)
{
ColorDialog1->Color = FPenColor;
if (ColorDialog1->Execute())
FPenColor = ColorDialog1->Color;
}
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
FPenThickness = dynamic_cast<TSpeedButton *>(Sender)->Tag;
}
This program enables you to draw lines, rectangles, and ellipses on the main form
of the application. You can select the type of shape you want to work with through
the menus or by clicking on speed buttons. The program also lets you set the color
of the shapes you want to draw and specify the width and color of the border around
the shape.
Pens and Brushes
When setting the color of a shape, you need to know that the filled area inside
a shape gets set to the color of the current Brush for the canvas. The line
that forms the border for the shape is controlled by the current Pen.
Here is how to set the current color and width of the Pen:
Canvas->Pen->Color = clBlue;
Canvas->Pen->Width = 5;
If you want to change the color of the Brush, you can write the following
code:
Canvas->Brush->Color = clYellow;
I don't work with it in this program, but you can assign various styles to a Brush,
as defined by the following enumerated type:
TBrushStyle { bsSolid, bsClear, bsHorizontal, bsVertical,
bsFDiagonal, bsBDiagonal, bsCross, bsDiagCross };
By default, a Brush has the bsSolid type.
To set the style of a Brush, you would write code that looks like this:
Canvas->Brush->Style = bsSolid;
Brushes also have Color and Bitmap properties. The
Bitmap property can be set to a small external bitmap image that defines
the pattern for Brush.
There is only one method of the TBrush object that you would be likely
to use. This method is called Assign, and it is used when you want to copy
the characteristics of one brush to another brush.
Pens have all the same properties as Brushes, but they add a
Width and Mode property. The Width property defines the
width of the Pen in pixels, and the Mode property defines the type
of operation to use when painting the Pen to the screen. These logical operations
will be discussed further in just a few moments.
Rubber Banding
Before reading this section, fire up the DrawShapes program and practice drawing
ellipses and rectangles onscreen so you can see how the rubber-band technique works.
If for some reason you can't run the DrawShapes program, open up Windows Paint and
draw some squares or circles by first clicking the appropriate icon from the Tools
menu. Watch the way these programs create an elastic square or circle that you can
drag around the desktop. Play with these shapes as you decide what dimensions and
locations you want for your geometric figures.
These tools appear to be difficult for a programmer to create, but thanks to the
Windows API, the code is relatively trivial. Following are the main steps involved,
each of which is explained in-depth later in this section:
- 1. When the user clicks the left mouse button, DrawShapes "memorizes"
the x and y coordinates of the WM_LBUTTONDOWN event.
2. As the user drags the mouse across the screen with the left button still
down, DrawShapes draws a square or circle each time it gets a WM_MOUSEMOVE
message. Just before painting each new shape, the program blanks out the previous
square or circle. The dimensions of the new shape are calculated by combining the
coordinates of the original WM_LBUTTONDOWN message with the current coordinates
passed in the WM_MOUSEMOVE message.
3. When the user generates a WM_LBUTTONUP message, DrawShapes paints
the final shape in the colors and pen size specified by the user.
Although this description obviously omits some important details, the outlines
of the algorithm should take shape in your mind in the form of only a few relatively
simple, logical strokes. Things get a bit more complicated when the details are mulled
over one by one, but the fundamental steps should be relatively clear.
NOTE: In the previous numbered list, I
give the names of the messages that generate VCL events. For instance, a WM_LBUTTONDOWN
message generates an OnMouseDown event, the WM_MOUSEMOVE message
generates an OnMouseMove event, and so on. I'm referring to the underlying
messages because it is a good idea to remember the connection between VCL events
and their associated Windows messages.
Zooming in on the details, here's a look at the response to a WM_LBUTTONDOWN
message:
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
FormMouseDown saves the location on which the mouse was clicked in the
first two fields of a TRect structure called FShapeRect. FShapeRect
is declared as a field of TForm1. I set the Right field of the
TRect structure to a large negative number so I know that this is the first
time that I need to start tracking the user's mouse movements. This is necessary
because the very first shape I draw needs to be treated as a special case.
The last thing done in the FormMouseDown method is to set a private variable
of TForm1 called FDrawing to True. This lets the program
know that the user has started a drawing operation.
After the left mouse button is pressed, the program picks up all WM_MOUSEMOVE
messages that come flying into DrawShape's ken:
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
The first line of the function uses one of several possible techniques for checking
to see if the left mouse button is down. If the button isn't down, the function ignores
the message. If it is down, the function gets the device context, sets the Pen's
drawing mode to pmNotXor, memorizes the current dimensions of the figure,
draws it, and releases the device context.
The Mode property of a TPen object sets the current drawing
mode in a manner similar to the way the last parameter in BitBlt sets the
current painting mode. You can achieve the same effect by directly calling the Windows
API function called SetROP2. In this case, DrawShape uses the logical
XOR and NOT operations to blit the elastic image to the
screen. This logical operation is chosen because it paints the old shape directly
on top of the original image, thereby effectively erasing each shape:
- If you XOR a square to the screen, the square will show up clearly.
- If you XOR that same image again in the same location, the image will
disappear.
Such are the virtues of simple logical operations in graphics mode.
NOTE: Aficionados of graphics logic will
note that the logical operation employed by DrawShapes is a variation on
the exclusive OR (XOR) operation. This variation ensures that the
fill in the center of the shape to be drawn won't blot out what's beneath it. The
Microsoft documentation explains the difference like this:
R2_XOR: final pixel = pen ^ screen pixel
R2_NOTXORPEN : final pixel = ~(pen ^ screen pixel)
This code tests to see whether the pixels to be XORed belong to a pen.
Don't waste too much time worrying about logical operations and how they work. If
they interest you, fine; if they don't, that's okay. The subject matter of this book
is programming, not logic.
If you were working directly in the Windows API, you would work with a constant called
RT_NOTXORPEN rather than pmNotXor. I have to confess that the VCL's
tendency to rename constants used by the Windows API is not a very winning trait.
Granted, the people in Redmond who came up with many of those identifiers deserve
some terrible, nameless, fate, but once the damage had been done it might have been
simpler to stick with the original constants. That way people would not have to memorize
two sets of identifiers, one for use with the VCL, and the other for use with the
Windows API. You cannot use the Windows constants in place of the VCL constants,
as the various identifiers do not map down to the same value.
Despite these objections, I still think it is wise to use the VCL rather than writing
directly to the Windows API. The VCL is much safer and much easier to use. The performance
from most VCL objects is great, and in many cases it will be better than what most
programmers could achieve writing directly to the Windows API.
Notice that FormMouseMove calls DrawShape twice. The first time,
it passes in the dimensions of the old figure that needs to be erased. That means
it XORs the same image directly on top of the original image, thereby erasing
it. Then FormMouseMove records the location of the latest WM_MOUSEMOVE
message and passes this new information to DrawShape, which paints the new
image to the screen. This whole process is repeated over and over again (at incredible
speeds) until the user lifts the left mouse button.
In the DrawImage function, Metaphor first checks to see which
shape the user has selected and then proceeds to draw that shape to the screen using
the current pen and fill color:
void __fastcall TForm1::DrawShape()
{
Canvas->Brush->Color = FBrushColor;
Canvas->Brush->Style = bsSolid;
Canvas->Pen->Color = FPenColor;
Canvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
This code sets the current Pen and Brush to the values chosen
by the user. It then uses a switch statement to select the proper type of
shape to draw to the screen. Most of these private variables such as FPenColor
are set by allowing the user to make selections from the menu. To see exactly how
this works, you can study the code of the application.
Notice that when drawing these shapes, there is no need to track the HDC of the
current Canvas. One of the primary goals of the TCanvas object
is to completely hide the HDC from the user. I discuss this matter in more depth
in the next section of the chapter, "To GDI or not to GDI."
The final step in the whole operation occurs when the user lifts his finger off
the mouse:
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Canvas->Pen->Mode = pmCopy;
DrawShape();
}
This code performs the following actions:
- A flag is set stating that the user has decided to stop drawing.
- The final dimensions of the shape are recorded.
- The mode for the Pen is switched from pmNotXor to the default
value, which is pmCopy.
- The final image is painted to the screen.
The code that paints the final shape takes into account the colors and the pen
thickness that the user selected with the menus.
Well, there you have it. That's how you draw shapes to the screen using the rubber-band
technique. Overall, if you take one thing at a time, the process isn't too complicated.
Just so you can keep those steps clear in your mind, here they are again:
- Remember where the WM_LBUTTONDOWN message took place.
- Draw the shape each time you get a WM_MOUSEMOVE message.
- Draw the final shape when you get a WM_LBUTTONUP message.
That's all there is to it.
To GDI or not to GDI
The VCL will never cut you off completely from the underlying Windows API code.
If you want to work at the Windows API level, you can do so. In fact, you can often
write code that mixes VCL and raw Windows API code.
If you want to access the HDC for a window, you can get at it through the Handle
property of the canvas:
MyOldPenHandle = SelectObject(Canvas->Handle, MyPen->Handle);
In this case you are copying the Handle of a Pen object into
the HDC of the TCanvas object.
Having free access to the Handle of the TCanvas object can be
useful at times, but the longer I use the VCL, the less inclined I am to use it.
The simple truth of the matter is that I now believe that it is best to let an object
of some sort handle all chores that require serious housekeeping. This course of
action allows me to rely on the object's internal logic to correctly track the resources
involved.
If you are not using the VCL, whenever you select something into an HDC, you need
to keep track of the resources pumped out of the HDC by the selection. When you are
done, you should then copy the old Handle back into the HDC. If you accidentally
lose track of a resource, you can upset the balance of the entire operating system.
Clearly, this type of process is error-prone and best managed by an object that can
be debugged once and reused many times. My options, therefore, are to either write
the object myself or use the existing code found in the VCL. In most cases, I simply
take the simplest course and use the excellent TCanvas object provided by
the VCL.
When I do decide to manage an interaction with the GDI myself, I often prefer
to get hold of my own HDC and ignore the TCanvas object altogether. The
reason I take this course is simply that the TCanvas object will sometimes
maintain the Canvas's HDC on its own, and I therefore can't have complete
control over what is happening to it.
Here is a simple example of how to use the GDI directly inside a VCL program:
HDC DC = GetDC(Form1->Handle);
HFONT OldFont = SelectObject(DC, Canvas->Font->Handle);
TextOut(DC, 1, 100, "Text", 4);
SelectObject(DC, OldFont);
ReleaseDC(Form1->Handle, DC);
If you look at the code shown here, you will see that I get my own DC by calling
the Windows API function GetDC. I then select a new font into the DC. Notice
that I use the VCL TFont object. I usually find it easier to manage Fonts,
Pens, and Brushes with VCL objects than with raw Windows API code.
However, if you want to create your own fonts with raw Windows API code, you are
free to do so.
Here is an example of creating a VCL Font object from scratch:
TFont *Font = new TFont();
Font->Name = "Time New Roman";
Font->Size = 25;
Font->Style = TFontStyles() << fsItalic << fsBold;
HDC DC = GetDC(Form1->Handle);
HFONT OldFont = SelectObject(DC, Font->Handle);
TextOut(DC, 1, 100, "Text", 4);
SelectObject(DC, OldFont);
ReleaseDC(Form1->Handle, DC);
delete Font;
This code allocates memory for a Font object, assigns some values to
its key properties, and then copies the Handle of the Font object
into the current DC. Notice that I still have to save the old Font handle
and copy it back into the DC when I am done with it. This is the type of operation
that I prefer to have handled by an object. When I am done with the example code
shown here, I delete the Font object I created.
Examples such as the ones you have just seen show that you can use the Windows
API by itself inside a VCL graphics operation, or you can combine VCL code with raw
GDI code. The course you choose will be dictated by your particular needs. My suggestion,
however, is to use the VCL whenever possible, and to fall back on the Windows API
only when strictly necessary. The primary reason for this preference is that it is
safer to use the VCL than to write directly to the Windows API. It is also easier
to use the VCL than the raw Windows API, but that argument has a secondary importance
in my mind.
Using TImage, Saving and Loading
Bitmaps
If you work with the DrawShapes program for awhile, you will find that it has
several weaknesses that cry out for correction. In particular, the program cannot
save an image to disk, and it cannot repaint the current image if you temporarily
cover it up with another program.
Fixing these problems turns out to be remarkably simple. The key functionality
to add to the program is all bound up in a single component called TImage.
This control provides you with a Canvas on which you can draw, and gives
you the ability to convert this Canvas into a bitmap.
The BitmapShapes program, shown in Listing 7.3, shows how to proceed in the creation
of an updated DrawShapes program that can save files to disk, and can automatically
redraw an image if you switch back to the main form from another program.
Listing 7.3. The main module for
the BitmapShapes program.
///////////////////////////////////////
// Main.cpp
// DrawShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FDrawing= False;
FCurrentShape = csRectangle;
FBrushColor = clBlue;
FPenColor = clYellow;
FPenThickness = 1;
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Image1->Canvas->Pen->Mode = pmCopy;
DrawShape();
}
void __fastcall TForm1::DrawShape()
{
Image1->Canvas->Brush->Color = FBrushColor;
Image1->Canvas->Brush->Style = bsSolid;
Image1->Canvas->Pen->Color = FPenColor;
Image1->Canvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
Image1->Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
Image1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
Image1->Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
Image1->Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
Image1->Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
FCurrentShape = TCurrentShape(dynamic_cast<TSpeedButton *>(Sender)->Tag);
}
void __fastcall TForm1::Brush1Click(TObject *Sender)
{
ColorDialog1->Color = FBrushColor;
if (ColorDialog1->Execute())
FBrushColor = ColorDialog1->Color;
}
void __fastcall TForm1::Pen1Click(TObject *Sender)
{
ColorDialog1->Color = FPenColor;
if (ColorDialog1->Execute())
FPenColor = ColorDialog1->Color;
}
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
FPenThickness = dynamic_cast<TSpeedButton *>(Sender)->Tag;
}
void __fastcall TForm1::Save1Click(TObject *Sender)
{
if (SaveDialog1->Execute())
{
Image1->Picture->SaveToFile(SaveDialog1->FileName);
}
}
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
Image1->Picture->LoadFromFile(OpenDialog1->FileName);
}
}
void __fastcall TForm1::Exit1Click(TObject *Sender)
{
Close();
}
The key change from the DrawShapes program is that all of the Canvas
operations are performed on the Canvas of a TImage control rather
than directly on the Canvas of a form:
Image1->Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
Image1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
A TImage control is designed explicitly for the type of operations undertaken
by this program. In particular, it maintains an internal bitmap into which images
are automatically drawn.
When it comes time to save the picture you have created, you can do so with one
line of code:
void __fastcall TForm1::Save1Click(TObject *Sender)
{
if (SaveDialog1->Execute())
{
Image1->Picture->SaveToFile(SaveDialog1->FileName);
}
}
Loading a file into memory from disk is also a simple one-line operation:
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
Image1->Picture->LoadFromFile(OpenDialog1->FileName);
}
}
Both of the preceding calls assume that you have dropped a TOpenDialog
onto the main form of your application. The TOpenDialog object is extremely
easy to use, so you should have no trouble learning how to use it from the online
help.
NOTE: When I opt not to explain an object
such as TOpenDialog, my intention is not to ignore the needs of this book's
readers, but rather to avoid inserting boring, repetitious information into the book.
If you really crave a reference other than the online help for this kind of information,
you should check the bookstore for introductory texts that cover this kind of material.
If you want a bit more flexibility, and desire to have a separate TBitmap
object which you can use for your own purposes, it is easy to create one from the
image you created with this program. To proceed, just create a TBitmap object,
and then use the Assign method to copy the picture from the TImage
control to your bitmap:
Graphics::TBitmap *Bitmap = new Graphics::TBitmap();
Bitmap->Assign(Image1->Picture->Graphic);
Bitmap->SaveToFile(SaveDialog1->FileName);
delete Bitmap;
In this example I save the bitmap to file and then delete the object when I am
through with it. In the context of the current program, this doesn't make a great
deal of sense. However, code like this demonstrates some of the functionality of
the TBitmap object. I qualify the reference to TBitmap with Graphics.Hpp,
because there are two TBitmap declarations in the header files included
in most BCB programs.
Working with Metafiles
Bitmaps are very convenient and are often the best way to work with graphic images.
However, BCB also lets you work with enhanced metafiles.
Metafiles are collections of shapes drawn into a device context. Internally, they
are little more than a list of GDI calls that can be played back on demand to create
a picture. Metafiles have two big advantages over bitmaps:
- They are very small. The same image that might take hundreds of KB to save in
bitmaps can often be saved in a metafile of just 10 or 20 thousand bytes.
- Because metafiles consist of a series of shapes, you can, at least potentially,
iterate through the list of shapes in a picture and edit them one at a time. This
can give you a very powerful ability to precisely edit the contents of a picture.
The disadvantage of metafiles is that they must consist only of a series of GDI
calls. As a result, you can't easily transform a photograph or a scanned image into
a metafile. Metafiles are best defined as a simple way of preserving text, or as
a simple means of storing a series of fairly simple shapes such as a line drawing.
In the hands of a good artist, however, you can get metafiles to show some fairly
powerful images. (See Figure 7.2.)
FIGURE
7.2. A grayscale version of a colorful
metafile that ships with Microsoft Office shows some of the power of this technology.
There are two types of metafiles available to Windows users. One has the extension
.WMF, and is primarily designed for use in the 16-bit world. The second
type of file has the extension .EMF, for Enhanced Metafile. These latter
types of files are designed for use in the 32-bit world. I find them much more powerful
than their 16-bit cousins.
The code shown in the forthcoming MetaShapes program is designed to work only
with enhanced metafiles. BCB will save a file as a regular metafile if you give it
an extension of .WMF, and it will save it as an enhanced metafile if you
give it an extension of .EMF. When using BCB, it is best to stick with enhanced
metafiles. If you have some .WMF files you want to use with BCB, you might
want to find a third-party tool that will convert your .WMF into an .EMF.
The MetaShapes program is very similar to the DrawShapes and BitmapShapes programs.
It has the same capabilities as the BitmapShapes program in terms of its capability
to save and preserve images, only it works with metafiles rather than bitmaps. Metafiles
are many times smaller than standard .BMP files.
There is no TImage control for metafiles, so you have to do a little
more work to make this system function properly. In particular, you have to explicitly
open a metafile and then draw directly into it, as shown in Listing 7.4 and 7.5.
Listing 7.4. The header for the
MetaShapes program.
///////////////////////////////////////
// Main.h
// MetaShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include <vcl\Dialogs.hpp>
enum TCurrentShape {csLine, csRectangle, csEllipse};
class TForm1 : public TForm
{
__published:
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *Open1;
TMenuItem *Save1;
TMenuItem *N1;
TMenuItem *Exit1;
TMenuItem *Shapes1;
TMenuItem *Rectangle2;
TMenuItem *Ellipse1;
TMenuItem *Colors1;
TMenuItem *Brush1;
TMenuItem *Pen1;
TPanel *Panel1;
TSpeedButton *SpeedButton1;
TSpeedButton *SpeedButton2;
TSpeedButton *SpeedButton3;
TSpeedButton *SpeedButton4;
TMenuItem *Lines1;
TColorDialog *ColorDialog1;
TSpeedButton *SpeedButton5;
TMenuItem *New1;
TPaintBox *PaintBox1;
TSaveDialog *SaveDialog1;
TOpenDialog *OpenDialog1;
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y);
void __fastcall SpeedButton1Click(TObject *Sender);
void __fastcall Brush1Click(TObject *Sender);
void __fastcall Pen1Click(TObject *Sender);
void __fastcall SpeedButton5Click(TObject *Sender);
void __fastcall New1Click(TObject *Sender);
void __fastcall Exit1Click(TObject *Sender);
void __fastcall PaintBox1Paint(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
void __fastcall Save1Click(TObject *Sender);
void __fastcall Open1Click(TObject *Sender);
private:
TRect FShapeRect;
TColor FPenColor;
TColor FBrushColor;
bool FDrawing;
TCurrentShape FCurrentShape;
int FPenThickness;
TMetafile* MyMetafile;
TMetafileCanvas* MyCanvas;
void __fastcall DrawShape();
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 7.5. The MetaShapes program
shows how to draw into a metafile and save it to disk.
///////////////////////////////////////
// Main.cpp
// MetaShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FDrawing= False;
FCurrentShape = csRectangle;
FBrushColor = clBlue;
FPenColor = clYellow;
FPenThickness = 1;
MyMetafile = new TMetafile;
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
PaintBox1->Canvas->Pen->Mode = pmCopy;
MyCanvas->Pen->Mode = pmCopy;
DrawShape();
}
}
void __fastcall TForm1::DrawShape()
{
PaintBox1->Canvas->Brush->Color = FBrushColor;
PaintBox1->Canvas->Brush->Style = bsSolid;
PaintBox1->Canvas->Pen->Color = FPenColor;
PaintBox1->Canvas->Pen->Width = FPenThickness;
MyCanvas->Brush->Color = FBrushColor;
MyCanvas->Brush->Style = bsSolid;
MyCanvas->Pen->Color = FPenColor;
MyCanvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
MyCanvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
MyCanvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
PaintBox1->Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
PaintBox1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
PaintBox1->Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
MyCanvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
PaintBox1->Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
MyCanvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
PaintBox1->Canvas->Pen->Mode = pmNotXor;
MyCanvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
FCurrentShape = TCurrentShape(dynamic_cast<TSpeedButton *>(Sender)->Tag);
}
void __fastcall TForm1::Brush1Click(TObject *Sender)
{
ColorDialog1->Color = FBrushColor;
if (ColorDialog1->Execute())
FBrushColor = ColorDialog1->Color;
}
void __fastcall TForm1::Pen1Click(TObject *Sender)
{
ColorDialog1->Color = FPenColor;
if (ColorDialog1->Execute())
FPenColor = ColorDialog1->Color;
}
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
FPenThickness = dynamic_cast<TSpeedButton *>(Sender)->Tag;
}
void __fastcall TForm1::New1Click(TObject *Sender)
{
delete MyCanvas;
delete MyMetafile;
MyMetafile = new TMetafile;
MyCanvas = new TMetafileCanvas(MyMetafile, 0);
InvalidateRect(Handle, NULL, True);
}
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
delete MyCanvas;
if (MyMetafile)
PaintBox1->Canvas->Draw(0,0,MyMetafile);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete MyCanvas;
}
void __fastcall TForm1::Exit1Click(TObject *Sender)
{
Close();
}
void __fastcall TForm1::Save1Click(TObject *Sender)
{
if (SaveDialog1->Execute())
{
delete MyCanvas;
MyMetafile->SaveToFile(SaveDialog1->FileName);
MyCanvas = new TMetafileCanvas(MyMetafile, 0);
MyCanvas->Draw(0, 0, MyMetafile);
}
}
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
FShapeRect = Rect(0, 0, 0, 0);
delete MyCanvas;
TFileStream *Stream = new TFileStream(OpenDialog1->FileName, fmOpenRead);
MyMetafile->LoadFromStream(Stream);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
PaintBox1->Canvas->Draw(0, 0, MyMetafile);
Stream->Free();
}
}
This program works just like the BitmapShapes program shown previously, in that
it lets you draw colored shapes to the screen and then save them to file. A screenshot
of the program is shown in Figure 7.3.
FIGURE
7.3. The MetaShapes program in action.
When working with metafiles, you need to create both a TMetafile object
and a TMetafileCanvas object.
MyMetafile = new TMetafile;
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
Notice that the TMetafileCanvas is explicitly associated with a particular
TMetafile object. Specifically, the TMetaFile object is passed
in as the first parameter of the TMetaFileCanvas object's constructor. These
two classes are designed to work in tandem, and both pieces must be present if you
want to create a metafile.
Here is a simple example of how to draw into a TMetafile canvas:
MyCanvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
This code looks exactly like any other call to the TCanvas::Ellipse,
only this time I am writing into a TMetafileCanvas rather than a form's
Canvas.
NOTE: The VCL preserves the Canvas
metaphor in a wide variety of contexts, such as when you are working with metafiles
or bitmaps. This enables you to learn one set of commands, and to then apply them
to a wide variety of objects. As you will see later in the book, this type of functionality
derives in part from a judicious and intelligent use of polymorphism.
Basing a suite of objects on one model enables users to quickly get up to speed on
new technologies. The underlying code for creating bitmaps is very different from
the code for creating metafiles. But the TCanvas object enables you to treat
each operation as if it were nearly identical. People who are interested in object
design should contemplate the elegance of this implementation.
If you only want to display a metafile, you can work solely with the TMetafile
object. However, if you want to create metafiles, to draw images into a metafile,
you need a TMetafileCanvas object. You can draw directly into a TMetafileCanvas,
but when you want to display the image to the user, you need to delete the object
so that its contents will be transferred into a metafile that can be displayed for
the user:
delete MyCanvas;
PaintBox1->Canvas->Draw(0,0,MyMetafile);
In this code I am using a PaintBox as the surface on which to display
the metafile. The code first deletes the TMetafileCanvas, thereby transferring
the painting from the TMetafileCanvas to the TMetafile. This implementation
is, I suppose, a bit awkward, but once you understand the principle involved, it
should not cause you any serious difficulty.
There is no particular connection between metafiles and the TPaintBox
object. In fact, I could just as easily have painted directly into a TForm.
The reason I chose TPaintBox is that it enables me to easily define a subsection
of a form that can be used as a surface on which to paint. In particular, part of
the form in the MetaShapes program is covered by a TPanel object.
To make sure that the user can see the entire canvas on which he will be painting,
I used a TPaintBox.
If you are interested in these matters, you might want to open up EXTCTRLS.HPP
and compare the declarations for TPaintBox and TImage. Both of
these controls are descendants of TGraphicControl, and both offer similar
functionality. The big difference between them is that TImage has an underlying
bitmap, while TPaintBox has a simpler, sparer architecture.
By now it has probably occurred to you that there is no simple means for displaying
a metafile to the user at the time it is being created. In particular, you can't
show it to the user without first deleting the TMetafileCanvas. To avoid
performing this action too often, I simply record the user's motions into both the
TMetafileCanvas and the Canvas for the main form:
PaintBox1->Canvas->Brush->Color = FBrushColor;
... // Code ommitted
MyCanvas->Brush->Color = FBrushColor;
...// Code ommitted
switch (FCurrentShape)
{
case csLine:
MyCanvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
MyCanvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
PaintBox1->Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
PaintBox1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
... // etc
This duplication of code, while bothersome, is not really terribly costly in terms
of what it is adding to the size of my executable. Needless to say, I use this technique
so that the user can see what he or she is painting.
If the user flips away from the program and covers it with another application,
I need to repaint the image when the user flips back to MetaShapes:
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
delete MyCanvas;
PaintBox1->Canvas->Draw(0,0,MyMetafile);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
}
As you can see, this code deletes the TMetafileCanvas and then paints
the current image to the screen. I then create a new TMetafileCanvas and
paint the contents of the current metafile into it:
MyCanvas->Draw(0, 0, MyMetafile);
This same process occurs when I load a metafile from disk:
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
delete MyCanvas;
TFileStream *Stream = new TFileStream(OpenDialog1->FileName, fmOpenRead);
MyMetafile->LoadFromStream(Stream);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
PaintBox1->Canvas->Draw(0, 0, MyMetafile);
}
}
In this code, I delete the contents of any current TMetafileCanvas, not
because I want to draw it, but because I want to create a new, blank surface on which
to paint. I then load a metafile from disk and create a new TMetafileCanvas
into which I can paint the contents of the freshly loaded metafile:
MyCanvas->Draw(0, 0, MyMetafile);
Finally, the program plays the contents of the metafile back to the user:
PaintBox1->Canvas->Draw(0, 0, MyMetafile);
When you are playing back fairly large metafiles, you can sometimes see the shapes
being painted to the screen one at a time. This helps to illustrate the fact that
metafiles are literally just lists of GDI calls with their related parameters and
device contexts carefully preserved. There are calls available that enable you to
walk through these lists and delete or modify individual items. However, I do not
take the code in this program quite that far. If you master this process, however,
you have the rudiments of a CAD or sophisticated drawing program on your hands.
The only thing left to discuss is saving metafiles to disk, which turns out to
be a simple process:
void __fastcall TForm1::Save1Click(TObject *Sender)
{
if (SaveDialog1->Execute())
{
delete MyCanvas;
MyMetafile->SaveToFile(SaveDialog1->FileName);
MyCanvas = new TMetafileCanvas(MyMetafile, 0);
MyCanvas->Draw(0, 0, MyMetafile);
}
}
This code deletes the current TMetafileCanvas, thereby assuring that
the TMetafile object is up-to-date. It then makes a call to the TMetafile::SaveToFile
method. Finally, it creates a new TMetafileCanvas and draws the contents
of the current metafile into it, which enables the user to continue editing the picture
after the save.
That is all I'm going to say about metafiles. Admittedly, this is a somewhat esoteric
subject from the point of view of many programmers. However, metafiles are a useful
tool that can be a great help if you want to save text, or a series of images, in
a small, compact file.
When I talk about saving text to a metafile, remember that all calls to draw text
on the screen are simply calls into the Windows GDI. You can therefore save these
calls in a metafile. Many fancy programs that enable you to quickly scroll through
large volumes of text are using metafiles as part of their underlying technology.
Working with Fonts
The raw Windows API code for working with fonts is quite complex. The VCL, however,
makes fonts easy to use.
You have already seen an example of using the TFont object. The basic
idea is simply that a TFont object has eight key properties called Color,
Handle, Height, Name, Pitch, PixelsPerInch,
Size, and Style. It is a simple matter to create a TFont
object and change its properties:
TFont *MyFont = new TFont();
MyFont->Name = "BookwomanSH";
MyFont->Size = 24;
MyFont->Style = TFontStyles() << fsBold;
Form1->Font = MyFont;
In this case, the programmer has no obligation to delete the TFont object,
because its ownership is assumed by the form in the last line. If you did not pass
the ownership of the object over to the Form, you would need to explicitly
delete the Font when you were through with it:
delete MyFont;
Most of the time, if you want to give the user the capability to select a font
at runtime, you should use the TFontDialog from the dialogs page of the
Component Palette. However, you can iterate through all the fonts on your system
by using the Fonts field of the TScreen object.
The TScreen object is used primarily for keeping track of the active
forms in your application and the current cursor. However, you can also use it for
iterating through all the available fonts on the system. These fonts are kept in
a TStringList object that is accessible through a property called Fonts:
ListBox1->Items = Screen->Fonts;
The screen object is created automatically by the VCL, and is available to all
applications as a global object.
Besides the screen fonts, some programmers might also be interested in accessing
the available printer fonts. These can be accessed through an object called TPrinter,
which is kept in a module called PRINTERS.HPP:
TPrinter *Printer = new TPrinter();
ListBox2->Items = Printer->Fonts;
delete Printer;
The BCBFonts program from this book's CD-ROM gives a simple example of how to
iterate through all the fonts on the system so the user can pick and choose from
them. The source code for the program is shown in Listing 7.6.
Listing 7.6. The main module for
the BCBFonts program shows how to view a list of the currently available fonts.
///////////////////////////////////////
// Main.cpp
// Project: BCBFonts
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <vcl\printers.hpp>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
ListBox1->Items = Screen->Fonts;
TPrinter *Printer = new TPrinter();
ListBox2->Items = Printer->Fonts;
delete Printer;
Panel4->Font->Size = TrackBar1->Position;
ListBox1->ItemIndex = 0;
Panel4->Caption = ListBox1->Items->Strings[0];
}
void __fastcall TForm1::ListBox1Click(TObject *Sender)
{
if (ListBox1->ItemIndex != -1)
{
Panel4->Font->Name = ListBox1->Items->Strings[ListBox1->ItemIndex];
Panel4->Caption = Panel4->Font->Name;
}
}
void __fastcall TForm1::TrackBar1Change(TObject *Sender)
{
Panel4->Font->Size = TrackBar1->Position;
}
This program has two list boxes. At program startup, the first list box is initialized
to display all the fonts currently available on the system. The right-hand list box
shows all the fonts currently available on the printer. As a rule, you don't need
to worry about coordinating the two sets of fonts, because Windows will handle that
problem for you automatically. However, if you are having trouble getting the right
fonts on your printer when working with a particular document, you might want to
compare these two lists and see if any obvious problems meet the eye.
If you select a font from the first list box in the BCBFonts program, its name
will appear in the caption of a TPanel object shown at the bottom of the
program. The Caption for the panel will use the font whose name is currently
being displayed, as shown in Figure 7.4. At the bottom of the BCBFonts program is
the TTrackBar object, which enables you to change the size of the current
font.
FIGURE
7.4. The BCBFonts program gives samples
of all the fonts on the system.
Working with Fractals
The graphics code shown so far in this section has been fairly trivial. The last
program I show in this chapter ups the ante a bit by showing how you can create fractals,
using the VCL graphics objects. This code should make it clear that the VCL tools
are quite powerful, and can be put to good use even in fairly graphics-intensive
programs.
The sample program on the CD, Fractals, is where you can find all of the code
discussed in this section of the chapter. The source for the program is shown in
Listings 7.7 through 7.13.
Listing 7.7. This program shows
how to use fractals to draw various shapes.
///////////////////////////////////////
// Main.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "Ferns.h"
#include "Squares1.h"
#include "Mandy.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------------
void __fastcall TForm1::Ferns1Click(TObject *Sender)
{
FernForm->ShowModal();
}
void __fastcall TForm1::Squares1Click(TObject *Sender)
{
SquaresForm = new TSquaresForm(this);
SquaresForm->ShowModal();
SquaresForm->Free();
}
void __fastcall TForm1::Mandelbrot1Click(TObject *Sender)
{
MandelForm = new TMandelForm(this);
MandelForm->ShowModal();
delete MandelForm;
}
//--------------------------------------------------------------------------
Listing 7.8. This header is for
a form that draws a fractal fern.
///////////////////////////////////////
// Ferns.h
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef FernsH
#define FernsH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
class TFernForm : public TForm
{
__published:
TTimer *Timer1;
void __fastcall Timer1Timer(TObject *Sender);
void __fastcall FormResize(TObject *Sender);
private:
void DoPaint();
float x, y;
int MaxIterations;
int Count;
int MaxX;
int MaxY;
public:
virtual __fastcall TFernForm(TComponent* Owner);
};
extern TFernForm *FernForm;
#endif
Listing 7.9. This form draws
a fractal that looks like a fern.
///////////////////////////////////////
// Ferns.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Ferns.h"
#pragma resource "*.dfm"
TFernForm *FernForm;
typedef float TDAry[4];
TDAry a = {0, 0.85, 0.2, -0.15};
TDAry b = {0, 0.04, -0.26, 0.28};
TDAry c = {0, -0.04, 0.23, 0.26};
TDAry d = {0.16, 0.85, 0.22, 0.24};
TDAry e = {0, 0, 0, 0};
TDAry f = {0, 1.6, 1.6, 0.44};
__fastcall TFernForm::TFernForm(TComponent* Owner)
: TForm(Owner)
{
Count = 0;
x = 0;
y = 0;
}
void TFernForm::DoPaint()
{
int k;
float TempX, TempY;
k = random(100);
if ((k > 0) && (k <= 85))
k = 1;
if ((k > 85) && (k <= 92))
k = 2;
if (k > 92)
k = 3;
TempX = a[k] * x + b[k] * y + e[k];
TempY = c[k] * x + d[k] * y + f[k];
x = TempX;
y = TempY;
if ((Count >= MaxIterations) || (Count != 0))
Canvas->Pixels[(x * MaxY / 11 + MaxX / 2)]
[(y * - MaxY / 11 + MaxY)] = clGreen;
Count = Count + 1;
}
void __fastcall TFernForm::Timer1Timer(TObject *Sender)
{
int i;
if (Count > MaxIterations)
{
Invalidate();
Count = 0;
}
for (i = 0; i <= 200; i++)
DoPaint();
}
void __fastcall TFernForm::FormResize(TObject *Sender)
{
MaxX = Width;
MaxY = Height;
MaxIterations = MaxY * 50;
Listing 7.10. The header for
the Squares form.
///////////////////////////////////////
// Squares.h
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef Squares1H
#define Squares1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#define BoxCount 25
class TSquaresForm : public TForm
{
__published:
TTimer *Timer1;
void __fastcall Timer1Timer(TObject *Sender);
void __fastcall FormShow(TObject *Sender);
void __fastcall FormHide(TObject *Sender);
private:
void DrawSquare(float Scale, int Theta);
public:
virtual __fastcall TSquaresForm(TComponent* Owner);
};
extern TSquaresForm *SquaresForm;
#endif
Listing 7.11. A form that draws
a hypnotic series of squares.
///////////////////////////////////////
// Squares.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <math.h>
#pragma hdrstop
#include "Squares1.h"
#pragma resource "*.dfm"
TColor Colors[BoxCount];
typedef TPoint TSquarePoints[5];
TSquarePoints Square =
{{-100, -100},{100, -100},{100, 100},
{-100, 100},{-100, -100}};
TSquaresForm *SquaresForm;
__fastcall TSquaresForm::TSquaresForm(TComponent* Owner)
: TForm(Owner)
{
int X;
Randomize;
Colors[1] = TColor(RGB(random(255), random(255), random(255)));
for (X = 2; X <= BoxCount; X++)
Colors[X] = TColor(Colors[X-1] + RGB(random(64), random(64), random(64)));
}
void TSquaresForm::DrawSquare(float Scale, int Theta)
{
int i;
float CosTheta, SinTheta;
TSquarePoints Path;
CosTheta = Scale * cos(Theta * M_PI / 180); // precalculate rotation & scaling
SinTheta = Scale * sin(Theta * M_PI / 180);
for (i = 0; i <= 4; i++)
{
Path[i].x = (Square[i].x * CosTheta + Square[i].y * SinTheta);
Path[i].y = (Square[i].y * CosTheta - Square[i].x * SinTheta);
}
Canvas->Polyline(Path, 4);
}
void __fastcall TSquaresForm::Timer1Timer(TObject *Sender)
{
int i;
float Scale = 1.0;
int Theta = 0;
SetViewportOrgEx(Canvas->Handle, ClientWidth / 2, ClientHeight / 2, NULL);
Canvas->Pen->Color = clWhite;
for (i = 1; i <= BoxCount; i++)
{
DrawSquare(Scale, Theta);
Theta = Theta + 10;
Scale = Scale * 0.85;
Canvas->Pen->Color = Colors[i];
}
for (i = BoxCount - 1; i >= 1; i--)
Colors[i] = Colors[i - 1];
Colors[0] = TColor(RGB(Colors[0] + random(64),
Colors[0] + random(64),
Colors[0] + random(64)));
}
void __fastcall TSquaresForm::FormShow(TObject *Sender)
{
Timer1->Enabled = True;
}
void __fastcall TSquaresForm::FormHide(TObject *Sender)
{
Timer1->Enabled = False;
}
Listing 7.12. The header for
the form that draws the Mandelbrot set.
///////////////////////////////////////
// Mandy.h
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MandyH
#define MandyH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
class TMandelForm : public TForm
{
__published:
TTimer *Timer1;
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormResize(TObject *Sender);
void __fastcall Timer1Timer(TObject *Sender);
void __fastcall FormDblClick(TObject *Sender);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X, int Y);
private:
int FDepth;
float FXRange; // The width and height of the
float FYRange; // mandlebrot plane. Starts at 3.
int FScrOrgX;
int FScrOrgY;
int FScrMaxX;
int FScrMaxY;
float FBaseOrgX;
float FBaseOrgY;
bool FQuitDrawing;
void GetOriginsAndWidths(float &XOrg, float &YOrg, float &XMax, float &YMax);
float Distance(float X, float Y);
void Calculate(float X, float Y, float &XIter, float &YIter);
int GetColor(int Steps);
TRect FShapeRect;
bool FDrawing;
void SetBoundary(int ScrX, int ScrY, int ScrX1, int ScrY1);
void SetMouseDownPos(int ScrX, int ScrY);
void SetMouseUpPos(int ScrX1, int ScrY1);
bool Run();
public: // User declarations
virtual __fastcall TMandelForm(TComponent* Owner);
void __fastcall DrawShape();
};
extern TMandelForm *MandelForm;
#endif
Listing 7.13. A simple form for
drawing the Mandelbrot set.
///////////////////////////////////////
// Mandy.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <math.h>
#pragma hdrstop
#include "Mandy.h"
#pragma resource "*.dfm"
TMandelForm *MandelForm;
__fastcall TMandelForm::TMandelForm(TComponent* Owner)
: TForm(Owner)
{
FXRange = 3;
FYRange = 3;
FBaseOrgX = -2.25;
FBaseOrgY = -1.5;
Width = 550;
Height = 400;
FDrawing = False;
}
void TMandelForm::GetOriginsAndWidths(float &XOrg, float &YOrg, float &XMax, float &YMax)
{
float VOrgX, VOrgY, VMaxX, VMaxY;
float XPercent, YPercent;
float MaxXPercent, MaxYPercent;
VOrgX = FScrOrgX;
VOrgY = FScrOrgY;
VMaxX = FScrMaxX;
VMaxY = FScrMaxY;
XPercent = VOrgX / Width;
YPercent = VOrgY / Height;
MaxXPercent = VMaxX / Width;
MaxYPercent = VMaxY / Height;
XOrg = (XPercent * FXRange) + FBaseOrgX;
YOrg = (YPercent * FYRange) + FBaseOrgY;
XMax = (MaxXPercent * FXRange) + FBaseOrgX;
YMax = (MaxYPercent * FYRange) + FBaseOrgY;
FBaseOrgX = XOrg;
FBaseOrgY = YOrg;
FXRange = XMax - XOrg;
FYRange = YMax - YOrg;
}
float TMandelForm::Distance(float X, float Y)
{
if ((X != 0.0) && (Y != 0.0))
return sqrt(pow(X, 2) + pow(Y, 2));
else
if (X == 0.0)
return abs(Y);
else
return abs(X);
};
void TMandelForm::Calculate(float X, float Y,
float &XIter, float &YIter)
{
float XTemp, YTemp;
XTemp = pow(XIter, 2) - pow(YIter, 2) + X;
YTemp = 2 * (XIter * YIter) + Y;
XIter = XTemp;
YIter = YTemp;
}
// Steps won't be larger than FDepth.
int TMandelForm::GetColor(int Steps)
{
int TopVal= 16777215; // RGB(255,255,255)
float Variation;
int Val;
Variation = TopVal / FDepth;
Val = Variation * Steps;
return Val;
}
void TMandelForm::SetBoundary(int ScrX, int ScrY, int ScrX1, int ScrY1)
{
FScrOrgX = ScrX;
FScrOrgY = ScrY;
FScrMaxX = ScrX1;
FScrMaxY = ScrY1;
};
void TMandelForm::SetMouseDownPos(int ScrX, int ScrY)
{
FScrOrgX = ScrX;
FScrOrgY = ScrY;
}
void TMandelForm::SetMouseUpPos(int ScrX1, int ScrY1)
{
FScrMaxX = ScrX1;
FScrMaxY = ScrY1;
if ((FScrMaxX - FScrOrgX) > 10)
Run();
}
void __fastcall TMandelForm::DrawShape()
{
Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
}
void __fastcall TMandelForm::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
if (Shift.Contains(ssRight))
FQuitDrawing = True;
else
SetMouseDownPos(X, Y);
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TMandelForm::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Canvas->Pen->Mode = pmCopy;
DrawShape();
SetMouseUpPos(X, Y);
}
bool TMandelForm::Run()
{
int i, j, Steps;
float XStep, YStep, XPos, YPos, XOrg, YOrg;
float XMax, YMax, XIter, YIter;
bool Done;
if (FDepth < 1)
FDepth = 50;
InvalidateRect(Handle, NULL, True);
GetOriginsAndWidths(XOrg, YOrg, XMax, YMax);
XStep = (XMax - XOrg) / Width;
YStep = (YMax - YOrg) / Height;
for (i = 0; i <= Width; i++)
for (j = 0; j <= Height; j++)
{
XPos = XOrg + i * XStep;
YPos = YOrg + j * YStep;
XIter = 0.0;
YIter = 0.0;
Steps =0;
Done = False;
do {
Calculate(XPos, YPos, XIter, YIter);
Steps++;
if (Distance(XIter, YIter) >= 2.0)
Done = True;
if (Steps == FDepth)
Done = True;
} while (!Done);
if (Steps < FDepth)
SetPixel(Canvas->Handle, i, j, GetColor(Steps));
Application->ProcessMessages();
if (FQuitDrawing)
break;
}
return True;
}
void __fastcall TMandelForm::FormResize(TObject *Sender)
{
SetBoundary(0, 0, ClientWidth, ClientHeight);
}
void __fastcall TMandelForm::Timer1Timer(TObject *Sender)
{
Timer1->Enabled = False;
Run();
}
void __fastcall TMandelForm::FormDblClick(TObject *Sender)
{
FQuitDrawing = True;
}
void __fastcall TMandelForm::FormMouseMove(TObject *Sender, TShiftState Shift,
int X, int Y)
{
if (FDrawing)
{
Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
//--------------------------------------------------------------------------
When this program is launched, you see the opening form shown in Figure 7.5. The
menu for this form will let you select secondary forms that draw fractal ferns (Figure
7.6), squares (Figure 7.7), and a rather hastily thrown together version of the Mandelbrot
set (Figure 7.8).
FIGURE
7.5. The main form for the Fractals program
features a bitmap made in Caligari's TrueSpace program.
FIGURE
7.6. A fractal fern drawn with VCL code.
FIGURE
7.7. These squares are animated and appear
to be swirling away from the user.
FIGURE
7.8. You can draw a square on the form
with the mouse to zoom in on any part of the Mandelbrot set.
This book is not about mathematics, so I will not give an in-depth explanation of
any of these forms. Instead, I will simply give a quick overview of each of the major
forms from the program. The key point to grasp is simply that you can use the VCL
to create programs that create rather elaborate graphics.
The Fern form uses a global variable called Count to check the
depth of detail to which the fern is drawn. When the detail reaches a certain point,
the image is erased, and the drawing is begun anew.
The output for the Fern program is handled by a single routine found in the DoPaint
method that draws a point to the screen:
Canvas->Pixels[(x * MaxY / 11 + MaxX / 2)]
[(y * - MaxY / 11 + MaxY)] = clGreen;
A Timer component dropped on the form calls the DoPaint method
at periodic intervals. By laboriously drawing pixels to the screen, one at a time,
the DoPaint method slowly builds up a fractal image of a fern. Needless
to say, it is the array-based math in the program that calculates where to draw the
pixels so that the fern takes on a "life of its own." The Squares
form creates the illusion that it is animating a set of squares by rotating them
one inside the next, so that they appear to be receding into the distance as they
spin away from the user. In fact, the code only calculates how to draw the squares
once, and then animates the palette with which the squares are colored:
for (i = 1; i <= BoxCount; i++)
{
DrawSquare(Scale, Theta);
Theta = Theta + 10;
Scale = Scale * 0.85;
Canvas->Pen->Color = Colors[i];
}
for (i = BoxCount - 1; i >= 1; i--)
Colors[i] = Colors[i - 1];
Colors[0] = TColor(RGB(Colors[0] + random(64),
Colors[0] + random(64),
Colors[0] + random(64)));
The first loop shown here draws each square in a different color from an array
of 25 different colors:
#define BoxCount 25
TColor Colors[BoxCount];
The second loop shown above shifts each color up the array by one notch. The final
statement sets the first element in the array to a new color.
When you watch the program in action, each color appears to be racing away from
the user down through the swirling maze of squares. Of course, the squares themselves
aren't really moving; it's just that the colors assigned to them change with each
iteration of the main loop in the program.
The Mandy form uses standard code to draw the Mandelbrot set. The user
can then left-click at one point and drag a square across a portion of the set that
he or she would look to see in more detail. When the user lets go of the left mouse
button, the highlighted part of the image will be redrawn at a high magnification.
The whole point of the Mandelbrot set, of course, is that there is no end to the
degree of detail with which you can view it. Of course, in this version, if you zoom
in close enough, some of the detail gets lost. You could add code to the program
that would fix this problem, but the current implementation should provide enough
fun for most users.
I provide only minimal code that checks all the possible errors the user could
make, such as clicking once on the form in the same spot:
void TMandelForm::SetMouseUpPos(int ScrX1, int ScrY1)
{
FScrMaxX = ScrX1;
FScrMaxY = ScrY1;
if ((FScrMaxX - FScrOrgX) > 10)
Run();
}
Here you can see that I check to make sure there is at least 10 pixels of difference
between the location where the user clicked the mouse and the location where he let
go of the mouse. This helps to protect the user from feeding invalid data to the
routines that calculate the area to draw. However, this code is not foolproof, and
you might find yourself in a situation where the code is taking a ridiculously long
time to finish its calculations. In that case, you can just double-click the form
to stop the drawing.
You have already seen the rubber band technology used to draw a square on the
form that delineates the next frame to be viewed. The code that performs that function
for the MandyForm is taken verbatim from the DrawShapes program shown earlier
in this chapter.
Summary
In this chapter you have had an overview of graphics programming with the VCL.
I have made no attempt to cover all facets of this subject, but hopefully you now
have enough information to meet most of your needs.
Key subjects covered in this chapter include the TCanvas object, used
for drawing shapes or words on a form or component surface. You also saw the TFont,
Tbrush, and TPen objects, which define the characteristics of the
shapes or letters drawn on a component. The TMetaFile and TBitmap
objects were also introduced, though I never did do anything with the simple and
easy-to-use TIcon object. The last section of the chapter had a bit of fun
drawing flashy images to the screen.
One related subject that was not covered in this chapter is the TImageList
object. If you are interested in this subject, you might want to look at the KdAddExplore
program found on the CD-ROM that accompanies this book. The program should be in
the Chap14 directory. The TImageList component provides a means
of storing a list of images in memory while taking up the minimum possible amount
of system resources. It is much less expensive to store five images in an image list
than it would be to use five separate bitmaps in your program.
|